前言
该文章是Vue复习姿势系列的第二篇,分享单选框组件的实现过程。
脚手架请参考第一篇: juejin.cn/post/701798…
组件示例地址: oversnail.github.io/over-snail-…
git地址: github.com/overSnail/o…
第一篇:Vue复习姿势系列之UI组件——按钮(Button)
介绍
基础组件,功能是提供一组备选项供用户选择,只能单选。
要实现的功能
属性
功能说明v-model/value绑定值禁用禁止使用单选框组提供一组选项给用户,v-model绑定在父级带有边框样式增强,并且提供四种尺寸按钮样式样式增加,提供四种尺寸
事件
事件名称说明回调参数change绑定值变化时的触发事件radio的value值
功能实现
1. 基础功能
src/packages目录下新建radio文件夹,文件夹内创建radio.vue和index.js。
src/styles目录下心新建radio.scss,并在src/styles/index.scss中引入。
我们用label标签将input[type=radio]及span包裹在一起。目的是让input的鼠标事件扩散到整个label上,这样即使隐藏了input元素,也能正常使用它的功能。
// radio.vue
export default {
name: 'myRadio',
data() {
return {
selected: false, // 是否被选中
}
},
props: {},
methods: {
onClick() {
this.selected = true
console.log('点击事件触发')
},
},
}
// radio.scss
@charset "UTF-8";
@import "common/var";
@import "mixins/mixins";
@include b(radio) {
display: inline-block;
box-sizing: border-box;
vertical-align: top;
font-size: $--font-size-large;
line-height: 20px;
height: 20px;
margin-right: 20px;
cursor: pointer;
&-input {
display: none;
}
&-icon {
box-sizing: border-box;
border: 1px solid #ddd;
height: 14px;
width: 14px;
border-radius: 50%;
background-color: #fff;
display: inline-block;
position: relative;
top: 2px;
&:after {
content: "";
position: absolute;
width: 4px;
height: 4px;
background-color: #fff;
left: 50%;
top: 50%;
border-radius: 50%;
transition: transform 0.2s;
transform: translate(-50%,-50%) scale(0);
}
&-selected {
background-color: $--color-primary;
&:after {
transform: translate(-50%,-50%) scale(1);
}
}
}
&-label {
display: inline-block;
margin-left: 3px;
}
&-selected {
color: $--color-primary;
}
}
2. 基础用法
首先是实现v-model和label两个属性
v-model语法糖拆开后实际上是v-bind:value和$emit("input")两个功能组成的。
考虑到初始化时v-model有可能有值,因此watch时需要immediate设置为true,这样radio初始化后能执行一次值同步。
// radio.vue
export default {
name: 'myRadio',
data() {
return {
selected: false, // 是否被选中
}
},
props: {
value: {
type: [String, Number, Boolean],
default: ""
},
label: {
type: [String, Number, Boolean],
default: ""
}
},
watch: {
// 初始化判断是否已被选中
value: {
handler(newVal) {
this.selected = this.value && this.value === this.label
},
immediate: true
},
},
methods: {
onClick() {
this.selected = true
this.$emit("input", this.label);
},
},
}
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/51376eca7a4e46e8857a03f4642449f4~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
3. 禁用状态
利用原生radio的disabled即可。
// button.vue 部分代码省略
......
props: {
// 禁用状态
disabled: {
type: Boolean,
default: false,
},
}
......
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f2a83624e787465898c08edcd11c34f6~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
4. 单选组框
该功能实现方式是创建个radio-group组件将radio包裹,radio功能由父级接管。
会用组件通信中的$dispatch和$broadcast,前者向上派发事件,后者向下广播事件。
vue组件生命周期是由内而外的:父created -> 子created -> 子mounted -> 父mounted,父组件要在created中监听事件,不能在mounted中监听。
radio-group的disabled具体逻辑比较简单,只需根据disabled值来调整radio组件内的myDisabled属性即可。而input[type="radio"]的disabled属性由myDisabled和disabled共同决定。
创建radio-group组件:
src/packages目录下新建radio-group文件夹,文件夹内创建radio-group.vue和index.js。
src/styles 目录下心新建radio-group.scss,并在src/styles/index.scss中引入。
// radio-group.vue
export default {
name: 'myRadioGroup',
data() {
return {
options: [], // 选项
}
},
props: {
value: {
type: [Boolean, String, Number],
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
watch: {
value: {
handler(newVal) {
this.syncValue(newVal)
},
immediate: true,
},
// 禁用状态
disabled: {
handler(newVal) {
this.syncOptionsDisable(newVal)
},
immediate: true,
},
},
created() {
// 监听on-radio-add事件,将radio实例存到options上
this.$on('on-radio-add', (radio) => {
this.options.push(radio)
this.syncValue(this.value)
this.syncOptionsDisable(this.disabled);
})
// 监听on-radio-remove事件,将radio实例从options中移除
this.$on('on-radio-remove', (radio) => {
this.options.splice(this.options.indexOf(radio), 1)
})
// 监听radio的选中事件,做value值同步
this.$on('on-radio-select', (radio) => {
this.syncValue(radio.label)
})
},
methods: {
/**
* @description value值同步
* @param {Boolean/String/Number} focusVal 选中值,为radio组件的label属性
*/
syncValue(focusVal) {
this.$emit('input', focusVal)
this.options.forEach((d) => {
d.selected = d.label === focusVal
})
},
/**
* @description 设置子选项的myDisabled属性
* @param {Boolean} disabled 是否禁用
*/
syncOptionsDisable(disabled) {
this.options.forEach((d) => {
d.myDisabled = disabled
})
},
},
}
调整radio组件代码,抛出事件给radio-group。
// radio.vue 省略部分代码
......
......
data() {
return {
selected: false, // 是否被选中
myDisabled: false, // 内部的禁用属性,交由父级控制
}
},
mounted() {
// 通知myRadioGroup组件调用on-radio-add方法,参数为当前radio实例
this.dispatch('myRadioGroup', 'on-radio-add', this)
},
beforeDestroy() {
// 移除时,调用myRadioGroup组件的on-radio-remove方法
this.dispatch('myRadioGroup', 'on-radio-remove', this)
},
methods: {
onClick() {
this.selected = true
this.$emit('input', this.label)
this.dispatch("myRadioGroup", 'on-radio-select', this)
},
},
......
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/651d80db9d0a47979a7238b01440cc12~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
5. 带有边框
给radio组件增加border属性,可以渲染带有边框的选项,此功能主要是对css的操作。
border生效时,size同样生效。需开发4种尺寸,此功能也是对css的操作。
// radio.vue 省略部分代码
......
......
// 工具函数,用于判断传入的值是否符合条件
import { oneOf } from '../../utils/assist';
export default {
props: {
......
// 是否绘制边框
border: {
type: Boolean,
default: false,
},
// 尺寸
size: {
validator(value) {
return oneOf(value, ['large', 'medium', 'small', 'mini'])
},
type: String,
default: 'medium',
},
......
}
}
// radio.scss 省略部分代码
@charset "UTF-8";
@import "common/var";
@import "mixins/mixins";
@include b(radio) {
......
// size和border相关样式
&-large-border {
height: 40px;
padding: 8px 8px 12px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
}
&-medium-border {
height: 36px;
padding: 6px 8px 10px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
}
&-small-border {
height: 32px;
padding: 4px 8px 8px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
font-size: $--font-size-medium;
}
&-mini-border {
height: 28px;
padding: 2px 8px 6px 8px;
border: 1px solid $--border-color;
border-radius: 4px;
font-size: $--font-size-medium;
}
}
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/331d969b62aa4bba9fc98470313cc478~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
6. 按钮样式
将radio渲染成按钮样式,也是对css的操作。
button属性设置给radio-group,由父级接管该功能。
// radio.vue 省略部分代码
......
data() {
return {
button: false, //按钮样式,由父级控制
}
}
// radio-group.vue 省略部分代码
......
props: {
......
// 是否启用按钮样式
button: {
type: Boolean,
default: false
}
},
watch: {
......
// 是否使用按钮样式
button: {
handler(newVal) {
this.syncOptionsButtonStyle(newVal)
},
immediate: true,
}
},
created() {
// 监听on-radio-add事件,将radio实例存到options上
this.$on('on-radio-add', (radio) => {
......
this.syncOptionsButtonStyle(this.button);
})
},
methods: {
/**
* @description 设置子选项的button属性,用以控制按钮样式
* @param {Boolean} value 是否设置
*/
syncOptionsButtonStyle(value) {
this.options.forEach((d) => {
d.button = value
})
}
}
// radio.scss 省略部分代码
@charset "UTF-8";
@import "common/var";
@import "mixins/mixins";
@include b(radio) {
......
// 按钮样式相关样式
&-large-button {
float: left;
background-color: #fff;
height: 40px;
line-height: 38px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
}
&-medium-button {
float: left;
background-color: #fff;
height: 36px;
line-height: 34px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
}
&-small-button {
float: left;
background-color: #fff;
height: 32px;
line-height: 30px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
font-size: $--font-size-medium;
}
&-mini-button {
float: left;
background-color: #fff;
height: 28px;
line-height: 26px;
padding: 0 15px 0 12px;
margin: 0;
border: 1px solid $--border-color;
font-size: $--font-size-medium;
}
}
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/52461005ef7b4f60987bb7e608811ac5~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp?)
结语
以上便是单选框组件的部分功能开发过程,单选框组功能中我们创建了新的组件radio-group作为父级,运用组件通讯中的广播与派发机制来协调父子之间的相互调用,以此完成v-model,disabled功能的实现。
涉及知识点
1.(Vue):class与style绑定
2.(Vue):v-model语法糖原理 3.(HTML):label标签
4. (vue):组件通讯-广播与派发
5. Vue中父子组件生命周期执行顺序回顾
|